package drds.common.db.xml_mapping.builder.xml;

import drds.common.db.xml_mapping.builder.*;
import drds.common.db.xml_mapping.io.Resources;
import drds.common.db.xml_mapping.mapping.ParameterMapping;
import drds.common.db.xml_mapping.mapping.ResultFlag;
import drds.common.db.xml_mapping.mapping.ResultMap;
import drds.common.db.xml_mapping.mapping.ResultMapping;
import drds.common.db.xml_mapping.parameter_and_result_value_type.JdbcType;
import drds.common.db.xml_mapping.parameter_and_result_value_type.TypeHandler;
import drds.common.db.xml_mapping.parsing.XmlNode;
import drds.common.db.xml_mapping.parsing.XmlNodeParser;
import drds.common.db.xml_mapping.reflection.MetaClass;
import drds.common.db.xml_mapping.session.Configuration;

import java.io.InputStream;
import java.io.Reader;
import java.util.*;


public class XmlMapperBuilder extends BaseBuilder {

  private final XmlNodeParser xmlNodeParser;
  private final MapperBuilderAssistant mapperBuilderAssistant;
  private final Map<String, XmlNode> sqlRefIdToXmlNodeMap;
  private final String resource;

  @Deprecated
  public XmlMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XmlNode> sqlRefIdToXmlNodeMap, String namespace) {
    this(reader, configuration, resource, sqlRefIdToXmlNodeMap);
    this.mapperBuilderAssistant.setNamespace(namespace);
  }

  @Deprecated
  public XmlMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XmlNode> sqlRefIdToXmlNodeMap) {
    this(new XmlNodeParser(reader, true, configuration.getVariables(), new XmlMapperEntityResolver()),
      configuration, resource, sqlRefIdToXmlNodeMap);
  }


  public XmlMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XmlNode> sqlRefIdToXmlNodeMap) {
    this(new XmlNodeParser(inputStream, true, configuration.getVariables(), new XmlMapperEntityResolver()),
      configuration, resource, sqlRefIdToXmlNodeMap);
  }

  private XmlMapperBuilder(XmlNodeParser xmlNodeParser, Configuration configuration, String resource, Map<String, XmlNode> sqlRefIdToXmlNodeMap) {
    super(configuration);
    this.mapperBuilderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.xmlNodeParser = xmlNodeParser;
    this.sqlRefIdToXmlNodeMap = sqlRefIdToXmlNodeMap;
    this.resource = resource;
  }

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(xmlNodeParser.evaluateNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingStatements();
  }


  private void configurationElement(XmlNode xmlNode) {
    try {
      String namespace = xmlNode.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      mapperBuilderAssistant.setNamespace(namespace);
      //
      parameterMapElement(xmlNode.evaluateXmlNodeList("/mapper/parameterMap"));
      resultMapElements(xmlNode.evaluateXmlNodeList("/mapper/resultMap"));
      //
      buildStatementFromContext(xmlNode.evaluateXmlNodeList("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }


  private void buildStatementFromContext(List<XmlNode> list) {
    for (XmlNode context : list) {
      final XmlStatementBuilder statementParser = new XmlStatementBuilder(configuration, mapperBuilderAssistant, context);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

  private void parsePendingResultMaps() {
    Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
    synchronized (incompleteResultMaps) {
      Iterator<ResultMapResolver> iterator = incompleteResultMaps.iterator();
      while (iterator.hasNext()) {
        try {
          iterator.next().resolve();
          iterator.remove();
        } catch (IncompleteElementException e) {
        }
      }
    }
  }


  private void parsePendingStatements() {
    Collection<XmlStatementBuilder> incompleteStatements = configuration.getIncompleteStatements();
    synchronized (incompleteStatements) {
      Iterator<XmlStatementBuilder> iterator = incompleteStatements.iterator();
      while (iterator.hasNext()) {
        try {
          iterator.next().parseStatementNode();
          iterator.remove();
        } catch (IncompleteElementException e) {
          // Statement is still missing a resource...
        }
      }
    }
  }


  private void parameterMapElement(List<XmlNode> xmlNodeList) {
    for (XmlNode parameterMapXmlNode : xmlNodeList) {
      String id = parameterMapXmlNode.getStringAttribute("id");
      String type = parameterMapXmlNode.getStringAttribute("parameter_and_result_value_type");
      Class<?> parameterClass = resolveClass(type);
      List<XmlNode> parameterXmlNodes = parameterMapXmlNode.evaluateXmlNodeList("parameter");
      List<ParameterMapping> parameterMappings = new ArrayList<>();
      for (XmlNode parameterXmlNode : parameterXmlNodes) {
        String property = parameterXmlNode.getStringAttribute("property");
        String javaType = parameterXmlNode.getStringAttribute("javaType");
        String jdbcType = parameterXmlNode.getStringAttribute("jdbcType");


        Class<?> javaTypeClass = resolveClass(javaType);
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);

        ParameterMapping parameterMapping = mapperBuilderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum);
        parameterMappings.add(parameterMapping);
      }
      mapperBuilderAssistant.addParameterMap(id, parameterClass, parameterMappings);
    }
  }

  private void resultMapElements(List<XmlNode> xmlNodeList) throws Exception {
    for (XmlNode xmlNode : xmlNodeList) {
      try {
        resultMapElement(xmlNode);
      } catch (IncompleteElementException e) {
        // ignore, it will be retried
      }
    }
  }

  private ResultMap resultMapElement(XmlNode resultMapXmlNode) throws Exception {
    return resultMapElement(resultMapXmlNode, Collections.emptyList(), null);
  }

  private ResultMap resultMapElement(XmlNode resultMapXmlNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {

    String type = resultMapXmlNode.getStringAttribute("parameter_and_result_value_type",
      resultMapXmlNode.getStringAttribute("ofType",
        resultMapXmlNode.getStringAttribute("resultType",
          resultMapXmlNode.getStringAttribute("javaType"))));
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
      typeClass = inheritEnclosingType(resultMapXmlNode, enclosingType);
    }

    List<ResultMapping> resultMappings = new ArrayList<>();
    resultMappings.addAll(additionalResultMappings);
    List<XmlNode> resultChildren = resultMapXmlNode.getChildrenXmlNodeList();
    for (XmlNode resultChild : resultChildren) {
      {
        List<ResultFlag> resultFlagList = new ArrayList<>();
        if ("id".equals(resultChild.getName())) {
          resultFlagList.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, resultFlagList));
      }
    }
    String id = resultMapXmlNode.getStringAttribute("id",
      resultMapXmlNode.getValueBasedIdentifier());
    String extend = resultMapXmlNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapXmlNode.getBooleanAttribute("autoMapping");
    ResultMapResolver resultMapResolver = new ResultMapResolver(mapperBuilderAssistant, id, typeClass, extend, resultMappings, autoMapping);
    try {
      return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

  protected Class<?> inheritEnclosingType(XmlNode resultMapXmlNode, Class<?> enclosingType) {
    if ("association".equals(resultMapXmlNode.getName()) && resultMapXmlNode.getStringAttribute("resultMap") == null) {
      String property = resultMapXmlNode.getStringAttribute("property");
      if (property != null && enclosingType != null) {
        MetaClass metaResultType = MetaClass.forClass(enclosingType);
        return metaResultType.getSetterType(property);
      }
    } else if ("case".equals(resultMapXmlNode.getName()) && resultMapXmlNode.getStringAttribute("resultMap") == null) {
      return enclosingType;
    }
    return null;
  }


  private ResultMapping buildResultMappingFromContext(XmlNode xmlNode, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    String property;
    {
      property = xmlNode.getStringAttribute("property");
    }
    String column = xmlNode.getStringAttribute("column");
    String javaType = xmlNode.getStringAttribute("javaType");
    String jdbcType = xmlNode.getStringAttribute("jdbcType");

    String nestedResultMap = xmlNode.getStringAttribute("resultMap",
      processNestedResultMappings(xmlNode, Collections.emptyList(), resultType));
    String notNullColumn = xmlNode.getStringAttribute("notNullColumn");
    String columnPrefix = xmlNode.getStringAttribute("columnPrefix");
    String typeHandler = xmlNode.getStringAttribute("typeHandler");
    String resultSet = xmlNode.getStringAttribute("resultSet");


    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return mapperBuilderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet);
  }

  private String processNestedResultMappings(XmlNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) throws Exception {
    if ("association".equals(context.getName())
      || "collection".equals(context.getName())
      || "case".equals(context.getName())) {
      if (context.getStringAttribute("select") == null) {
        validateCollection(context, enclosingType);
        ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
        return resultMap.getId();
      }
    }
    return null;
  }

  protected void validateCollection(XmlNode context, Class<?> enclosingType) {
    if ("collection".equals(context.getName()) && context.getStringAttribute("resultMap") == null
      && context.getStringAttribute("javaType") == null) {
      MetaClass metaResultType = MetaClass.forClass(enclosingType);
      String property = context.getStringAttribute("property");
      if (!metaResultType.hasSetter(property)) {
        throw new BuilderException(
          "Ambiguous collection parameter_and_result_value_type for property '" + property + "'. You must specify 'javaType' or 'resultMap'.");
      }
    }
  }

  private void bindMapperForNamespace() {
    String namespace = mapperBuilderAssistant.getNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound parameter_and_result_value_type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }

}
